﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DateAndTime.cs" company="Data Ductus Malmö">
//   (c) 2010 - 2012 Data Ductus Malmö AB
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace DataDuctus.Snmp.Persistor.Core.Types
{
  using System;
  using System.ComponentModel;
  using System.Globalization;

  using Lextm.SharpSnmpLib;

  /// <summary>
  /// Converts a <see cref="DateTime"/> to a encoded <see cref="OctetString"/> as per TC DateAndTime
  /// (defined in SNMPv2-TC)
  /// </summary>
  /// <remarks>
  /// DateAndTime ::= TEXTUAL-CONVENTION
  /// DISPLAY-HINT "2d-1d-1d,1d:1d:1d.1d,1a1d:1d"
  /// STATUS       current
  /// DESCRIPTION
  /// "A date-time specification.
  /// field  octets  contents                  range
  /// -----  ------  --------                  -----
  /// 1      1-2   year*                     0..65536
  /// 2       3    month                     1..12
  /// 3       4    day                       1..31
  /// 4       5    hour                      0..23
  /// 5       6    minutes                   0..59
  /// 6       7    seconds                   0..60
  /// (use 60 for leap-second)
  /// 7       8    deci-seconds              0..9
  /// 8       9    direction from UTC        '+' / '-'
  /// 9      10    hours from UTC*           0..13
  /// 10      11    minutes from UTC          0..59
  /// 
  /// * Notes:
  /// - the value of year is in network-byte order
  /// - daylight saving time in New Zealand is +13
  /// 
  /// For example, Tuesday May 26, 1992 at 1:30:15 PM EDT would be
  /// displayed as:
  /// 
  /// 1992-5-26,13:30:15.0,-4:0
  /// 
  /// Note that if only local time is known, then timezone
  /// information (fields 8-10) is not present."
  /// SYNTAX       OCTET STRING (SIZE (8 | 11))
  /// </remarks>
  public sealed class DateAndTime : TypeConverter
  {
    #region Public Methods and Operators

    /// <summary>
    /// The can convert from.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="sourceType">
    /// The source type.
    /// </param>
    /// <returns>
    /// The System.Boolean.
    /// </returns>
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
      if (sourceType == typeof(DateTime))
      {
        return true;
      }

      return base.CanConvertFrom(context, sourceType);
    }

    /// <summary>
    /// The can convert to.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="destinationType">
    /// The destination type.
    /// </param>
    /// <returns>
    /// The System.Boolean.
    /// </returns>
    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
      if (destinationType == typeof(DateTime))
      {
        return true;
      }

      return base.CanConvertTo(context, destinationType);
    }

    /// <summary>
    /// Converts a <see cref="DateTime"/> to a DateAndTime encoded <see cref="OctetString"/>.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="culture">
    /// Using culture.
    /// </param>
    /// <param name="value">
    /// The <see cref="DateTime"/> value.
    /// </param>
    /// <returns>
    /// A <see cref="OctetString"/> encoded as DateAndTime as defined in SNMPv2-TC.
    /// </returns>
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
      if (!(value is DateTime))
      {
        return base.ConvertFrom(context, culture, value);
      }

      TimeZone tz = TimeZone.CurrentTimeZone;

      DateTime dt = tz.ToUniversalTime((DateTime)value); // TODO: is this correct even for already utc DateTime:s?
      TimeSpan tzOffset = tz.GetUtcOffset(dt);

      byte[] year = BitConverter.GetBytes((short)dt.Year);

      var buff = new byte[11];
      buff[0] = year[1];
      buff[1] = year[0];
      buff[2] = (byte)dt.Month;
      buff[3] = (byte)dt.Day;
      buff[4] = (byte)dt.Hour;
      buff[5] = (byte)dt.Minute;
      buff[6] = (byte)dt.Second;
      buff[7] = (byte)((byte)dt.Millisecond / 100); // deci second
      buff[8] = tzOffset < TimeSpan.Zero ? (byte)'-' : (byte)'+';
      buff[9] = (byte)tzOffset.Hours;
      buff[10] = (byte)tzOffset.Minutes;

      return new OctetString(buff);
    }

    /// <summary>
    /// Converts a <see cref="OctetString"/> encoded as DateAndTime (per SNMPv2-TC definition) to a <see cref="DateTime"/>.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="culture">
    /// Using culture.
    /// </param>
    /// <param name="value">
    /// The <see cref="OctetString"/> to be converted.
    /// </param>
    /// <param name="destinationType">
    /// A <see cref="DateTime"/>.
    /// </param>
    /// <returns>
    /// A <see cref="DateTime"/> either with timezone info or localtime depending on the inparam <see cref="OctetString"/> length (8 or 11).
    /// </returns>
    public override object ConvertTo(
      ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
      if (!(value is OctetString) || (destinationType != typeof(DateTime)))
      {
        return base.ConvertTo(context, culture, value, destinationType);
      }

      byte[] buff = ((OctetString)value).GetRaw();

      if (buff.Length != 8 && buff.Length != 11)
      {
        throw new ArgumentException("The octet string must either be 8 or 11 octets long", "value");
      }

      var yearBuff = new byte[2];
      yearBuff[0] = buff[1]; // Network byte order
      yearBuff[1] = buff[0];

      int year = BitConverter.ToUInt16(yearBuff, 0);
      var month = (int)buff[2];
      var day = (int)buff[3];
      var hour = (int)buff[4];
      var minute = (int)buff[5];
      var second = (int)buff[6];
      int mills = buff[7] * 100;

      if (buff.Length < 11)
      {
        return new DateTime(year, month, day, hour, minute, second, mills, culture.Calendar, DateTimeKind.Local);
      }

      bool negativeTzOffset = buff[8] == '-';

      var tzOffset = new TimeSpan(0, negativeTzOffset ? -buff[9] : buff[9], buff[10], 0);

      return new DateTimeOffset(year, month, day, hour, minute, second, mills, culture.Calendar, tzOffset).DateTime;
    }

    #endregion
  }
}